package dpss.client;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.ParserConfigurationException;

import org.omg.CORBA.ORB;
import org.omg.CORBA.ShortHolder;
import org.omg.CORBA.StringHolder;
import org.omg.CORBA.ORBPackage.InvalidName;
import org.omg.CosNaming.NamingContextExt;
import org.omg.CosNaming.NamingContextExtHelper;
import org.omg.CosNaming.NamingContextPackage.CannotProceed;
import org.omg.CosNaming.NamingContextPackage.NotFound;
import org.xml.sax.SAXException;

import dpss.commons.CustomLogger;
import dpss.commons.Player;
import dpss.commons.Repo;
import dpss_api_corba.DpssApi;
import dpss_api_corba.DpssApiHelper;
import dpss_api_corba.DpssApiPackage.DpssException;

/**
 * 
 * Implemented Client using corba API
 * 
 * @author Shewetaank Indora
 * 
 */
public class Client {

	private static CustomLogger logger = CustomLogger.getInstance();

	private static final String FILE_PROPERTIES = "project.properties";

	private static String ipAddress = null;
	private static String localHostAddress = null;
	private static Properties prop;
	private static Repo repo;
	private static DpssApi dpssApi;
	private static Player me;

	private static HashMap<String, String> myServer;

	/**
	 * The main function of client program
	 * 
	 * @param args
	 * @throws MalformedURLException
	 */
	public static void main(String[] args) throws MalformedURLException {

		me = null;

		logger.out("========== Welcome to the Distributed Player System! ==================");
		logger.out("---------- Developed by Musab M. Mirza & Shewetaank Indora --------------");
		logger.out("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
		logger.out("\n");

		loadProperties();
		initLogFolder();

		repo = new Repo(prop.getProperty("repo.path"));

		getServerInfo();

		connectToCORBA();
	

		showMainMenu();
	}

	/**
	 * The main menu when entering the program
	 */
	private static void showMainMenu() {

		// Menu
		logger.out("\nPlease select a choice: \n");
		logger.out("1. Register new player");
		logger.out("2. Sign in");
		logger.out("3. Exit");

		// Selection
		try {
			int choice = readInt();
			switch (choice) {
			case 1:
				logger.cls();
				showRegistrationForm();
				break;
			case 2:
				logger.cls();
				showLoginForm();
				break;
			case 3:
				logger.out("Have a nice day!");
				logger.log("Closed client");
				System.exit(0);
				break;
			default:
				logger.out("ERROR - Please enter an integer");
				showMainMenu();
			}
		} catch (Exception e) {
			logger.out("ERROR - Please enter an integer");
			showMainMenu();
		}
	}

	/**
	 * Shows the login form
	 */
	private static void showLoginForm() {

		// Menu
		logger.out("\nPlease enter your login details: \n");

		logger.out("Your username");
		String username = readLine();

		logger.out("Your password");
		String password = readLine();

		// Attempt to login on server
		try {
			StringHolder fName = new StringHolder();
			StringHolder lName = new StringHolder();
			ShortHolder age = new ShortHolder();
			dpssApi.playerSignIn(username, password, ipAddress, fName, lName,
					age);

			logger.out("here");
			
			try {
				me = new Player(username, password, (int) age.value, ipAddress,
						fName.value, lName.value, true);
			} catch (Exception e) {
				logger.out("ERROR - " + e.getMessage());
				logger.log("ERROR - Failed to sign in server"
						+ myServer.get("name"));
				showLoginForm();
			}
			logger.cls();
			logger.out("Welcome " + me.getFirstName() + " " + me.getLastName()
					+ "! \n");
			logger.log("Signed in " + me.getUsername() + " on "
					+ myServer.get("name"));
			CustomLogger.changeLogFile((String) prop
					.get("client.playerlogfolder")
					+ "//"
					+ me.getUsername()
					+ ".log");
			logger.log("Signed in on " + myServer.get("name"));
			if (me.getUsername().equalsIgnoreCase("admin"))
				showLoggedInMenu("admin");
			else
				showLoggedInMenu("player");
		} catch (DpssException e) {
			logger.out("ERROR - " + e.reason);
			logger.log("ERROR - Failed to sign in server"
					+ myServer.get("name"));
			showLoginForm();
		}

	}

	/**
	 * The registration form to register a user
	 */
	private static void showRegistrationForm() {
		// Menu
		logger.out("\nPlease enter your registration details: \n");

		logger.out("Your first name: ");
		String fName = readLine();

		logger.out("Your last name ");
		String lName = readLine();

		logger.out("Your age");
		int age = 20;
		try {
			age = readInt();
		} catch (Exception e) {
			logger.out("ERROR - Invalid age entered, assuming 20");
		}

		logger.out("Your desired username");
		String username = readLine();

		logger.out("Your desired password");
		String password = readLine();

		// Attempt to register player on server
		try {
			dpssApi.createPlayerAccount(fName, lName, (short) age, username,
					password, ipAddress);

			try {
				me = new Player(username, password, age, ipAddress, fName,
						lName, true);
			} catch (Exception e) {
				logger.out("ERROR - " + e.getMessage());
				logger.log("ERROR - Failed to register on server");
				showRegistrationForm();
			}
			logger.cls();
			logger.out("Welcome " + me.getFirstName() + " " + me.getLastName()
					+ "! You were registered!\n");
			logger.log("Registered " + me.getUsername() + " on "
					+ myServer.get("name"));
			CustomLogger.changeLogFile((String) prop
					.get("client.playerlogfolder")
					+ "//"
					+ me.getUsername()
					+ ".log");
			logger.log("Registered account on " + myServer.get("name"));
			showLoggedInMenu("player");
		} catch (DpssException e) {
			logger.out("ERROR - " + e.reason);
			logger.log("ERROR - Failed to register on server");
			showRegistrationForm();
		}
	}

	/**
	 * After registration or signin, this menu is shown
	 */
	private static void showLoggedInMenu(String mode) {

		/************************
		 * PLAYER'S MENU
		 ************************/
		if (mode.equalsIgnoreCase("player")) {

			// Menu
			logger.out("\nPlease select a choice: \n");
			logger.out("1. Transfer Account");
			logger.out("2. Sign out and exit");

			// Selection
			int choice = 0;
			try {
				choice = readInt();
			} catch (Exception e) {
				logger.out("ERROR - Please enter an integer");
				showLoggedInMenu("player");
			}

			switch (choice) {
			case 1: 
				logger.cls();
				showTransferAccountMenu();
				break;
			case 2:
				try {
					dpssApi.playerSignOut(me.getUsername(), me.getPassword());
				} catch (DpssException e) {
					logger.out("ERROR - "
							+ e.reason);
					logger.out("ERROR - Could not sign out");
					showLoggedInMenu("player");
				}
				logger.out("Have a nice day!");
				logger.log("Signed out and closed client");
				System.exit(0);
				break;
			default:
				logger.out("ERROR - Invalid choice");
				showLoggedInMenu("player");
			}

			
			
		/************************
		 * ADMIN'S MENU
		 ************************/
		} else if (mode.equalsIgnoreCase("admin")) {

			// Menu
			logger.out("\nPlease select a choice: \n");
			logger.out("1. Get players status on all servers");
			logger.out("2. Suspend an account");
			logger.out("3. Signout and exit");

			// Selection
			int choice = 0;
			try {
				choice = readInt();
			} catch (Exception e) {
				logger.out("ERROR - Please enter an integer");
				showLoggedInMenu("admin");
			}

			switch (choice) {
			case 1:
				try {
					StringHolder playersStatus = new StringHolder();
					dpssApi.getPlayersStatus(me.getUsername(),
							me.getPassword(), ipAddress, playersStatus);
					logger.cls();
					logger.out("Players status: " + playersStatus.value);
					logger.log("Fetched players status");
					showLoggedInMenu("admin");
				} catch (DpssException e) {
					logger.out("ERROR - "
							+ e.reason);
					showLoggedInMenu("admin");
				}
				break;
			case 2:
				logger.cls();
				showSuspendAccountForm();
				break;
			case 3:
				try {
					dpssApi.playerSignOut(me.getUsername(), me.getPassword());
				} catch (DpssException e) {
					logger.out("ERROR - "
							+ e.reason);
					showLoggedInMenu("admin");
				}
				logger.out("Have a nice day!");
				logger.log("Signed out and closed client");
				System.exit(0);
				break;
			default:
				logger.out("ERROR - Invalid choice");
				showLoggedInMenu("admin");
			}
		}
	}
	
	/**
	 * Form to take inputs for suspending an account
	 */
	private static void showSuspendAccountForm() {

		// Menu
		logger.out("\nEnter the username to remove from the current server ("+myServer.get("name")+"): \n");

		String usernameToSuspend = readLine();
		
		// Attempt to login on server
		try {
			dpssApi.suspendAccount(me.getUsername(), me.getPassword(), me.getIpAddress(), usernameToSuspend);
			logger.out(usernameToSuspend+" was successfully removed this server");
			logger.log(usernameToSuspend+" was removed from " + myServer.get("name"));
		} catch (DpssException e) {
			logger.out("ERROR - " + e.reason);
			logger.log("ERROR - Unable to suspend username '"+usernameToSuspend+"' from " + myServer.get("name"));
		}
		showLoggedInMenu("admin");
		
	}

	/**
	 * Shows the menu to transfer account
	 */
	private static void showTransferAccountMenu() {
		
		logger.out("Servers available to transfer account: \n");
		HashSet<HashMap<String, String>> allServers = null;
		try {
			allServers = repo.getAllServers();
		} catch (ParserConfigurationException | SAXException | IOException e1) {
			logger.out("ERROR: Could not connect to repo");
			System.exit(0);
		}

		
		for(HashMap<String, String> singleServer : allServers){
			if(singleServer.get("id").equals(myServer.get("id"))){
				continue;
			}
			else{
				logger.out("Server ID: " + singleServer.get("id"));
			}
		}
		logger.out("\n");
		
		logger.out("Enter the server id to which you want to transfer the account-");
		String userInput = readLine();
		
		if (userInput.equalsIgnoreCase(myServer.get("id"))){
			logger.out("ERROR: This is your current server, please select from the choices given.");
			showTransferAccountMenu();
		}
		
		HashMap<String, String> selectedServer = null;
		for(HashMap<String, String> singleServer : allServers){
			if (userInput.equalsIgnoreCase(singleServer.get("id"))){
				selectedServer = singleServer;
			}
		}
		
		if (selectedServer == null){
			logger.out("ERROR: Invalid choice, please select again");
			showTransferAccountMenu();
		}
		
		// Valid choice
		
		String newipAddress = createTargetServerIp(selectedServer.get("iprange"));
		try {
			dpssApi.transferAccount(me.getUsername(), me.getPassword(), ipAddress, newipAddress);
			logger.out("Your account was transferred to " + selectedServer.get("name") + " successfully. Please restart the client");
			logger.log("Account was transferred from " + myServer.get("name") + " to " + selectedServer.get("name"));
			System.exit(0);
		} catch (DpssException e) {
			logger.out("ERROR: " + e.reason );
			logger.log("ERROR: " + e.reason );
			showTransferAccountMenu();
		}

	}


	/**
	 * Manipulates the given IP to target server IP
	 * 
	 * @param iprange
	 * @return
	 */
	private static String createTargetServerIp(String iprange) {
		
		int temp = ipAddress.indexOf(".");
		return iprange + "." + ipAddress.substring(temp + 1);
	}

	/**
	 * Makes the connection to the Corba server
	 */
	private static void connectToCORBA() {

		String[] corbaArgs = prop.getProperty("corba.args").split("\\s+");

		ORB orb = ORB.init(corbaArgs, null);

		org.omg.CORBA.Object objRef = null;
		try {
			objRef = orb.resolve_initial_references("NameService");
		} catch (InvalidName e) {
			logger.log("ERROR: Could not resolve NameService");
			System.exit(0);
		}

		NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);

		String name = "GameServer_" + myServer.get("id");

		try {
			dpssApi = DpssApiHelper.narrow(ncRef.resolve_str(name));
		} catch (NotFound e) {
			logger.log("ERROR: The server is not found");
			e.printStackTrace();
			System.exit(0);
		} catch (CannotProceed e) {
			logger.log("ERROR: Could not proceed with the CORBA initialization");
			e.printStackTrace();
			System.exit(0);
		} catch (org.omg.CosNaming.NamingContextPackage.InvalidName e) {
			logger.log("ERROR: Invalid Name");
			e.printStackTrace();
			System.exit(0);
		}
	}

	/**
	 * Gets the server information from the server
	 */
	private static void getServerInfo() {

		while (true) {

			getIpAddress();

			// Attempt to connect to the server
			try {
				HashMap<String, String> serverItem;
				try {
					serverItem = repo.getServerInfo(ipAddress);
					if (serverItem == null) {
						logger.out("ERROR - No server was found for your IP address");
						logger.log("ERROR - No server was found for IP address: "
								+ ipAddress);
						continue;
					}

					logger.out("Your server will be: " + serverItem.get("name")
							+ ", attempting to connect...");
					logger.log("Retrieved server address: " + localHostAddress
							+ ", attempting connection");
					myServer = serverItem;
					break;

				} catch (IOException | SAXException e) {
					logger.out("ERROR - Could not connect to the repo, exiting");
					logger.out("ERROR - Could not connect to repo");
					System.exit(0);
				}

			} catch (ParserConfigurationException ex) {

				ex.printStackTrace();
			}
		}
	}

	/**
	 * Gets the User's IP address
	 */
	private static void getIpAddress() {
		
		logger.out("Please enter your IP address");

		while (true) {
			String userInput = readLine();
			if (isValidIP(userInput)) {
				ipAddress = userInput;
				break;
			}
			logger.out("ERROR - Invalid IP, please re-enter");
		}
	}

	/**
	 * Reads line from console and returns it
	 * 
	 * @return
	 */
	private static String readLine() {
		System.out.print("==> ");
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String read = null;
		try {
			read = br.readLine();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return read;
	}

	/**
	 * Reads an integer from console and returns it
	 * 
	 * @return
	 * @throws Exception
	 */
	private static int readInt() throws Exception {
		System.out.print("==> ");
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		try {
			try {
				int i = Integer.parseInt(br.readLine());
				return i;
			} catch (NumberFormatException nfe) {
				throw new Exception("Invalid number");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return 0;
	}

	/**
	 * The method validate the IP Address provided by the user.
	 * 
	 * @param ip
	 * @return
	 */
	public static boolean isValidIP(String ip) {
		Pattern pattern = Pattern
				.compile("^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
						+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
						+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
						+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
		Matcher matcher = pattern.matcher(ip);
		return matcher.matches();
	}

	/**
	 * Loads up the log folder
	 */
	private static void initLogFolder() {

		File theDir = new File((String) prop.get("client.playerlogfolder"));
		if (!theDir.exists()) {
			theDir.mkdir();
		}
	}

	/**
	 * Loads the properties of the client into the instance prop variable
	 */
	private static void loadProperties() {
		prop = new Properties();
		InputStream input = null;
		try {
			input = new FileInputStream(FILE_PROPERTIES);
			prop.load(input);
		} catch (IOException ex) {
			logger.out("ERROR - Unable to load properties file");
		} finally {
			if (input != null) {
				try {
					input.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
